package com.junjie.blibli.service.impl;

import com.junjie.blibli.Entity.*;



import com.junjie.blibli.Exception.http.ConditionException;
import com.junjie.blibli.dao.VideoDao;
import com.junjie.blibli.result.PageResult;
import com.junjie.blibli.service.FileService;
import com.junjie.blibli.service.UserService;
import com.junjie.blibli.service.VideoService;
import com.junjie.blibli.service.userCoinService;
import com.junjie.blibli.util.FastDFSUtil;
import com.junjie.blibli.util.ImageUtil;
import com.junjie.blibli.util.IpUtil;
import eu.bitwalker.useragentutils.UserAgent;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

import org.apache.mahout.cf.taste.common.TasteException;
import org.apache.mahout.cf.taste.impl.common.FastByIDMap;
import org.apache.mahout.cf.taste.impl.model.GenericDataModel;
import org.apache.mahout.cf.taste.impl.model.GenericPreference;
import org.apache.mahout.cf.taste.impl.model.GenericUserPreferenceArray;
import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;
import org.apache.mahout.cf.taste.impl.recommender.GenericItemBasedRecommender;
import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;
import org.apache.mahout.cf.taste.impl.similarity.UncenteredCosineSimilarity;
import org.apache.mahout.cf.taste.model.DataModel;
import org.apache.mahout.cf.taste.model.PreferenceArray;
import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood;
import org.apache.mahout.cf.taste.recommender.RecommendedItem;
import org.apache.mahout.cf.taste.recommender.Recommender;
import org.apache.mahout.cf.taste.similarity.ItemSimilarity;
import org.apache.mahout.cf.taste.similarity.UserSimilarity;

@Service
public class VideoServiceImpl implements VideoService {
    @Autowired
    private VideoDao videoDao;

    @Autowired
    private FastDFSUtil fastDFSUtil;

    @Autowired
    private userCoinService userCoinService;

    @Autowired
    private UserService userService;

    @Autowired
    private ImageUtil imageUtil;

    @Autowired
    private FileService fileService;

    //帧数截取跨度，每隔16帧进行一次截取
    private static final int FRAME_NO = 16;

    @Override
    @Transactional
    public void addVideos(Video video) {
        Date date = new Date();
        video.setCreateTime(date);
        //添加到视频表
        videoDao.addVideos(video);
        Long videoId = video.getId();
        List<VideoTag> tagList = video.getVideoTagList();
        tagList.forEach(item->{
            item.setCreateTime(date);
            item.setVideoId(videoId);
        });
        //添加到标签表
        videoDao.batchAddVideoTags(tagList);
    }

    @Override
    public PageResult<Video> pageListVideos(Integer size, Integer no, String area) {
        if(size==null || no==null) throw new ConditionException("参数异常！");
        HashMap<String, Object> params = new HashMap<>();
        params.put("start",(no-1)*size);
        params.put("limit",size);
        params.put("area",area);
        List<Video> list = new ArrayList<>();
        Integer total = videoDao.pageCountVideos(params);
        if(total>0){
            list = videoDao.pageListVideos(params);
        }
        return new PageResult<>(total,list);
    }

    @Override
    public void ViewVideoOnLineBySlices(HttpServletRequest request, HttpServletResponse response, String url) throws Exception {
        fastDFSUtil.ViewVideoOnLineBySlices(request,response,url);
    }

    //用户点赞视频
    public void addVideoLike(Long videoId, Long userId) {
        Video video = videoDao.getVideoById(videoId);
        if(video == null){
            throw new ConditionException("非法视频！");
        }
        VideoLike videoLike = videoDao.getVideoLikeByVideoIdAndUserId(videoId, userId);
        if(videoLike != null){
            throw new ConditionException("已经赞过！");
        }
        videoLike = new VideoLike();
        videoLike.setVideoId(videoId);
        videoLike.setUserId(userId);
        videoLike.setCreateTime(new Date());
        videoDao.addVideoLike(videoLike);
    }

    //取消点赞
    public void deleteVideoLike(Long videoId, Long userId) {
        videoDao.deleteVideoLike(videoId, userId);
    }

    //获取视频点赞数量
    public Map<String, Object> getVideoLikes(Long videoId, Long userId) {
        Long count = videoDao.getVideoLikes(videoId);
        //判断当前用户是否点赞，这里不用管userId是否为null，为null就是没点赞，因为都没登录
        VideoLike videoLike = videoDao.getVideoLikeByVideoIdAndUserId(videoId, userId);
        boolean like = videoLike != null;
        Map<String, Object> result = new HashMap<>();
        //点赞数量
        result.put("count", count);
        //自己是否点赞
        result.put("like", like);
        return result;
    }

    //添加收藏
    @Transactional
    public void addVideoCollection(VideoCollection videoCollection, Long userId) {
        Long videoId = videoCollection.getVideoId();
        Long groupId = videoCollection.getGroupId();
        if(videoId == null || groupId == null){
            throw new ConditionException("参数异常！");
        }
        Video video = videoDao.getVideoById(videoId);
        if(video == null){
            throw new ConditionException("非法视频！");
        }
        //删除原有视频收藏
        videoDao.deleteVideoCollection(videoId, userId);
        //添加新的视频收藏
        videoCollection.setUserId(userId);
        videoCollection.setCreateTime(new Date());
        videoDao.addVideoCollection(videoCollection);
    }

    //删除该视频收藏记录
    public void deleteVideoCollection(Long videoId, Long userId) {
        videoDao.deleteVideoCollection(videoId, userId);
    }

    //获取视频收藏量与该用户是否收藏
    public Map<String, Object> getVideoCollections(Long videoId, Long userId) {
        Long count = videoDao.getVideoCollections(videoId);
        VideoCollection videoCollection = videoDao.getVideoCollectionByVideoIdAndUserId(videoId, userId);
        boolean like = videoCollection != null;
        Map<String, Object> result = new HashMap<>();
        result.put("count", count);
        result.put("like", like);
        return result;
    }


    /**
     * 投币操作
     * 首先要声明的，这里投币跟B站有点不一样，B站的硬币是投过一次后就不允许投了，然后只能投1到2个
     * 这里的投币操作是，投币想投几个就投几个，投完之后还可以投
     */
    @Transactional
    public void addVideoCoins(VideoCoin videoCoin, Long userId) {
        Long videoId = videoCoin.getVideoId();
        Integer amount = videoCoin.getAmount();
        if(videoId == null){
            throw new ConditionException("参数异常！");
        }
        //查看视频是否存在
        Video video = videoDao.getVideoById(videoId);
        if(video == null){
            throw new ConditionException("非法视频！");
        }
        //查询当前登录用户是否拥有足够的硬币
        Integer userCoinsAmount = userCoinService.getUserCoinsAmount(userId);
        userCoinsAmount = userCoinsAmount == null ? 0 : userCoinsAmount;
        if(amount > userCoinsAmount){
            throw new ConditionException("硬币数量不足！");
        }
        //查询当前登录用户对该视频已经投了多少硬币
        VideoCoin dbVideoCoin = videoDao.getVideoCoinByVideoIdAndUserId(videoId, userId);
        //新增视频投币
        if(dbVideoCoin == null){
            videoCoin.setUserId(userId);
            videoCoin.setCreateTime(new Date());
            videoDao.addVideoCoin(videoCoin);
        }else{
            Integer dbAmount = dbVideoCoin.getAmount();
            dbAmount += amount;
            //更新视频投币
            videoCoin.setUserId(userId);
            videoCoin.setAmount(dbAmount);
            videoCoin.setUpdateTime(new Date());
            videoDao.updateVideoCoin(videoCoin);
        }
        //更新用户当前硬币总数
        userCoinService.updateUserCoinsAmount(userId, (userCoinsAmount-amount));
    }

    //查询视频投币数量
    public Map<String, Object> getVideoCoins(Long videoId, Long userId) {
        Long count = videoDao.getVideoCoinsAmount(videoId);
        VideoCoin videoCollection = videoDao.getVideoCoinByVideoIdAndUserId(videoId, userId);
        boolean like = videoCollection != null;
        Map<String, Object> result = new HashMap<>();
        result.put("count", count);
        result.put("like", like);
        return result;
    }

    //添加视频评论
    public void addVideoComment(VideoComment videoComment, Long userId) {
        Long videoId = videoComment.getVideoId();
        if(videoId == null){
            throw new ConditionException("参数异常！");
        }
        Video video = videoDao.getVideoById(videoId);
        if(video == null){
            throw new ConditionException("非法视频！");
        }
        videoComment.setUserId(userId);
        videoComment.setCreateTime(new Date());
        videoDao.addVideoComment(videoComment);
    }

    //查询该视频下所有的评论
    public PageResult<VideoComment> pageListVideoComments(Integer size, Integer no, Long videoId) {
        Video video = videoDao.getVideoById(videoId);
        if(video == null){
            throw new ConditionException("非法视频！");
        }
        Map<String, Object> params = new HashMap<>();
        params.put("start", (no-1)*size);
        params.put("limit", size);
        params.put("videoId", videoId);
        //该视频评论总数量，直接查一级评论总数量就行了
        Integer total = videoDao.pageCountVideoComments(params);
        List<VideoComment> list = new ArrayList<>();
        if(total > 0){
            //分页查询出所有的一级评论
            list = videoDao.pageListVideoComments(params);
            //先把所有一级评论id拿出来，然后批量查询二级评论
            List<Long> parentIdList = list.stream().map(VideoComment::getId).collect(Collectors.toList());
            List<VideoComment> childCommentList = videoDao.batchGetVideoCommentsByRootIds(parentIdList);
            //批量查询用户信息
            //userIdList是所有一二级评论的用户ID，这里用Set集合非常细节
            //因为要取出二级评论的Id，以及被评论的Id，所以刚好可以用set去重
            Set<Long> userIdList = list.stream().map(VideoComment::getUserId).collect(Collectors.toSet());
            Set<Long> replyUserIdList = childCommentList.stream().map(VideoComment::getUserId).collect(Collectors.toSet());
            Set<Long> childUserIdList = childCommentList.stream().map(VideoComment::getReplyUserId).collect(Collectors.toSet());
            userIdList.addAll(replyUserIdList);
            userIdList.addAll(childUserIdList);
            //批量查询所有人的信息
            List<UserInfo> userInfoList = userService.batchGetUserInfoByUserIds(userIdList);
            //wc，Stream流还可以这样用，将所有人信息用 userId:userInfo 转换成Map集合
            Map<Long, UserInfo> userInfoMap = userInfoList.stream().collect(Collectors.toMap(UserInfo :: getUserId, userInfo -> userInfo));
            //遍历一级评论列表，把相关联的二级评论加上，用户信息加上
            list.forEach(comment -> {
                Long id = comment.getId();
                List<VideoComment> childList = new ArrayList<>();
                childCommentList.forEach(child -> {
                    if(id.equals(child.getRootId())){
                        //这两条是冗余字段，需要手动设置
                        child.setUserInfo(userInfoMap.get(child.getUserId()));
                        child.setReplyUserInfo(userInfoMap.get(child.getReplyUserId()));
                        childList.add(child);
                    }
                });
                comment.setChildList(childList);
                comment.setUserInfo(userInfoMap.get(comment.getUserId()));
            });
        }
        return new PageResult<>(total, list);
    }

    //获取视频详情
    public Map<String, Object> getVideoDetails(Long videoId) {
        Video video =  videoDao.getVideoDetails(videoId);
        Long userId = video.getUserId();
        User user = userService.getUserInfo(userId);
        //视频用户对应的头像还是要有吧
        UserInfo userInfo = user.getUserInfo();
        Map<String, Object> result = new HashMap<>();
        result.put("video", video);
        result.put("userInfo", userInfo);
        return result;
    }

    //添加视频观看记录
    @Override
    public void addVideoView(VideoView videoView, HttpServletRequest request) {
        Long userId = videoView.getUserId();
        Long videoId = videoView.getVideoId();
        //生成clientId
        String agent = request.getHeader("User-Agent");
        UserAgent userAgent = UserAgent.parseUserAgentString(agent);
        String clientId = String.valueOf(userAgent.getId());
        //获取用户IP
        String ip = IpUtil.getIP(request);
        Map<String, Object> params = new HashMap<>();
        //如果userId是null说明是游客模式，那么就用ip+clientId区分游客
        if(userId != null){
            params.put("userId", userId);
        }else{
            params.put("ip", ip);
            params.put("clientId", clientId);
        }
        Date now = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        params.put("today", sdf.format(now));
        params.put("videoId", videoId);
        //查询该用户同一天是否观看过该视频，每天只能加一播放量
        VideoView dbVideoView = videoDao.getVideoView(params);
        if(dbVideoView == null){
            //添加观看记录
            videoView.setIp(ip);
            videoView.setClientId(clientId);
            videoView.setCreateTime(new Date());
            videoDao.addVideoView(videoView);
        }
    }

    @Override
    public Integer getVideoViewCounts(Long videoId) {
        return videoDao.getVideoViewCounts(videoId);
    }

    /**
     * 基于用户的协同推荐
     * @param userId 用户id
     */
    @Override
    public List<Video> recommend(Long userId) throws TasteException {
        //获取所有的用户偏好 就是具体分数的一些东西
        List<UserPreference> list = videoDao.getAllUserPreference();
        //创建数据模型
        DataModel dataModel = this.createDataModel(list);
        //获取用户相似程度
        UserSimilarity similarity = new UncenteredCosineSimilarity(dataModel);
        //测试---打印出11号用户和12号用户的相似程度
        System.out.println(similarity.userSimilarity(11, 12));
        //获取用户邻居，第一个参数是想要的邻居数量，第二个是相似度，第三个是数据模型
        UserNeighborhood userNeighborhood = new NearestNUserNeighborhood(2, similarity, dataModel);
        //获取出邻居
        long[] ar = userNeighborhood.getUserNeighborhood(userId);
        //构建推荐器--这个构建器是基于用户的，看方法名也看的出来咯
        Recommender recommender = new GenericUserBasedRecommender(dataModel, userNeighborhood, similarity);
        //推荐视频，第二个参数是推荐几个
        List<RecommendedItem> recommendedItems = recommender.recommend(userId, 5);
        //这个抽出来的元素Id，实际上就是我们的videoId
        List<Long> itemIds = recommendedItems.stream().map(RecommendedItem::getItemID).collect(Collectors.toList());
        //最后根据这些id去批量查询就行了
        return videoDao.batchGetVideosByIds(itemIds);
    }

    /**
     * 基于内容的协同推荐
     * @param userId 用户id
     * @param itemId 参考内容id（根据该内容进行相似内容推荐）
     * @param howMany 需要推荐的数量
     */
    public List<Video> recommendByItem(Long userId, Long itemId, int howMany) throws TasteException {
        List<UserPreference> list = videoDao.getAllUserPreference();
        //创建数据模型
        DataModel dataModel = this.createDataModel(list);
        //获取内容相似程度
        ItemSimilarity similarity = new UncenteredCosineSimilarity(dataModel);
        GenericItemBasedRecommender genericItemBasedRecommender = new GenericItemBasedRecommender(dataModel, similarity);
        // 物品推荐相拟度，计算两个物品同时出现的次数，次数越多任务的相拟度越高
        List<Long> itemIds = genericItemBasedRecommender.recommendedBecause(userId, itemId, howMany)
                .stream()
                .map(RecommendedItem::getItemID)
                .collect(Collectors.toList());
        //推荐视频
        return videoDao.batchGetVideosByIds(itemIds);
    }

    private DataModel createDataModel(List<UserPreference> userPreferenceList) {
        FastByIDMap<PreferenceArray> fastByIdMap = new FastByIDMap<>();
        //将用户偏好集合转换成Map集合
        Map<Long, List<UserPreference>> map = userPreferenceList.stream().collect(Collectors.groupingBy(UserPreference::getUserId));
        //然后列表拿出来进行遍历
        Collection<List<UserPreference>> list = map.values();
        for(List<UserPreference> userPreferences : list){
            //把每个元素换成新的类
            GenericPreference[] array = new GenericPreference[userPreferences.size()];
            for(int i = 0; i < userPreferences.size(); i++){
                UserPreference userPreference = userPreferences.get(i);
                //根据userId，videoId，value创建新对象，并放到数组里面
                GenericPreference item = new GenericPreference(userPreference.getUserId(), userPreference.getVideoId(), userPreference.getValue());
                array[i] = item;
            }
            //全部放到结果Map中，存放了所有用户对应的得分啥的
            fastByIdMap.put(array[0].getUserID(), new GenericUserPreferenceArray(Arrays.asList(array)));
        }
        return new GenericDataModel(fastByIdMap);
    }

    //弹幕遮罩
    @Override
    public List<VideoBinaryPicture> convertVideoToImage(Long videoId, String fileMd5) throws Exception{
        //根据文件md5查询相关路径类型md5等等
        Files files = fileService.getFileByMD5(fileMd5);
        //下载到本地的路径，就是要先把视频下载到服务器本地
        String filePath = "F:\\iotest\\commonDownLoad\\" + videoId + "." + files.getType();
        //下载文件本身
        fastDFSUtil.downLoadFile(files.getUrl(), filePath);
        //通过文件路径，生成对应的实体类，该实体类是实现视频截取帧的关键了
        FFmpegFrameGrabber fFmpegFrameGrabber = FFmpegFrameGrabber.createDefault(filePath);
        fFmpegFrameGrabber.start();
        //获取视频总的帧数
        int ffLength = fFmpegFrameGrabber.getLengthInFrames();
        //每一帧可以用Frame存储
        Frame frame;
        //转换器，把相对应的内容进行一个转换，具体就是把帧Frame转换侧成我们想要的样子
        Java2DFrameConverter converter = new Java2DFrameConverter();
        //计数器
        int count = 1;
        //处理好后的视频每一帧，要保存到数据库的
        List<VideoBinaryPicture> pictures = new ArrayList<>();
        //遍历每一帧
        for(int i=1; i<= ffLength; i ++){
            //获取每一帧的时间戳
            long timestamp = fFmpegFrameGrabber.getTimestamp();
            //grabImage()截取帧变为图片的方法
            frame = fFmpegFrameGrabber.grabImage();
            //判断计数器是否等于当前遍历帧的序号
            //因为我们遍历视频的时候，视频可能很大，帧数很大，没必要把每一帧截取
            //我们可以每隔16帧或者32帧进行截取，这里我们设定是每16帧截取一次
            if(count == i){
                //判断当前帧是否存在
                if(frame == null){
                    throw new ConditionException("无效帧");
                }
                //每帧对应的图片保存到本地
                BufferedImage bufferedImage = converter.getBufferedImage(frame);
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                ImageIO.write(bufferedImage, "png", os);
                //需要图片的输入流
                InputStream inputStream = new ByteArrayInputStream(os.toByteArray());
                //输出黑白剪影文件
                //创建临时文件，方便对文件进行一个输出
                java.io.File outputFile = java.io.File.createTempFile("convert-" + videoId + "-", ".png");
                //调用百度API生成黑白剪影文件
                BufferedImage binaryImg = imageUtil.getBodyOutline(bufferedImage, inputStream);
                //写到我们的临时生成文件中去
                ImageIO.write(binaryImg, "png", outputFile);
                //有的浏览器或网站需要把图片白色的部分转为透明色，使用以下方法可实现
                imageUtil.transferAlpha(outputFile, outputFile);
                //上传视频剪影文件
                String imgUrl = fastDFSUtil.uploadCommonFile(outputFile, "png");
                VideoBinaryPicture videoBinaryPicture = new VideoBinaryPicture();
                videoBinaryPicture.setFrameNo(i);
                videoBinaryPicture.setUrl(imgUrl);
                videoBinaryPicture.setVideoId(videoId);
                videoBinaryPicture.setVideoTimestamp(timestamp);
                //把处理好后的帧放到列表里面
                pictures.add(videoBinaryPicture);
                //每次累加16帧，每次16帧才截取1次
                count += FRAME_NO;
                //删除临时文件
                outputFile.delete();
            }
        }
        //删除临时文件
        File tmpFiles = new File(filePath);
        tmpFiles.delete();
        //批量添加视频剪影文件到数据库里面
        videoDao.batchAddVideoBinaryPictures(pictures);
        return pictures;
    }

    @Override
    public List<VideoTag> getVideoTagsByVideoId(Long videoId) {
        return videoDao.getVideoTagsByVideoId(videoId);
    }

    @Override
    public void deleteVideoTags(List<Long> tagIdList, Long videoId) {
        videoDao.deleteVideoTags(tagIdList, videoId);
    }

    @Override
    public List<VideoBinaryPicture> getVideoBinaryImages(Map<String, Object> params) {
        return videoDao.getVideoBinaryImages(params);
    }
}
